using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.IO;    //DEBUG

namespace ClassifierLib.Classifier.Filters
{
    /// <summary>
    /// (selection filter equivelent to explore strategy)
    /// </summary>
    public class ExploreSelect : SelectFilter
    {
        
        private Dictionary<Action, double> wheelSum = new Dictionary<Action, double>();
        private Dictionary<Action, double> wheelFitness = new Dictionary<Action, double>();


        public ExploreSelect()
        {
            // this ensures that any rules created have a key,value for the required statistics
            Rule.InitialStatistics["lastIterationInGA"] = 0;

            GeneticAlgorithmThreshold = 25;
            Chi = 0.5;
            Mu = 0.1;
            MaxPopulation = 1000;       //TODO: This should be moved to the LCS.cs 
            SubsumptionAgeThreshold = 20;
            
            this.Name = "Explore Select";
            if (!RegisteredTypes.Contains(GetType()))
                RegisteredTypes.Add(GetType());   
        }

        //DEBUG - enable to write output of subsumption for lifetime of LCS
        //private TextWriter tw = new StreamWriter(System.IO.Directory.GetCurrentDirectory().Substring(0,System.IO.Directory.GetCurrentDirectory().LastIndexOf("LCS"))+"Debug Output Files\\subsumption_" + DateTime.Now.ToString("yyMMdd_HHmmss") + ".csv");
        
        //DEBUG
        public void CloseDebugFile()
        {
            //DEBUG - enable to write output of subsumption for lifetime of LCS
            //try
            //{
            //    tw.Close();
            //}
            //catch (Exception)
            //{ }
        }

        public override void SelectionMethod(Population MatchSet)
        {
            //RouletteSelection();
            RandomSelection(MatchSet);      //Determine random action from actions available in the match set
        }

        public override Population Apply(Population Population, Population MatchSet, ClassifierLib.Environment.State State, Action Action)
        {
            Population ActionSet = base.Apply(Population, MatchSet, State, Action);

            bool isGeneticAlgorithmRequired = (AverageTimeSinceGA(ActionSet) > GeneticAlgorithmThreshold);
           
            if (isGeneticAlgorithmRequired)
            {
                RunGA(Population, ActionSet);
                RunSubsumption(Population, ActionSet);
            }

            // modify default value for statistic so that any new Rules obtain the current iteration
            //Rule.InitialStatistics["lastIterationInGA"] = iteration; // this seems to make the GA very unreliable


            return ActionSet;

        }

        public override void CalculateStatistics(Population Population, Population Selected)
        {
            ClearStatisticsVariables();

            foreach (Rule r in Selected)
            {
                double wheelSumIncrement = r.Statistics["prediction"] * r.Statistics["fitness"];
                if (wheelSum.ContainsKey(r.Action))
                {
                    wheelSum[r.Action] += wheelSumIncrement;
                }
                else
                {
                    wheelSum[r.Action] = wheelSumIncrement;
                }
                double wheelFitnessIncrement = r.Statistics["fitness"];
                if (wheelFitness.ContainsKey(r.Action))
                {
                    wheelFitness[r.Action] += wheelFitnessIncrement;
                }
                else
                {
                    wheelFitness[r.Action] = wheelFitnessIncrement;
                }
            }
        }

        /// <summary>
        /// Resets all dictionaries and constants used during statistics calculations
        /// </summary>
        private void ClearStatisticsVariables()
        {
            wheelSum.Clear();
            wheelFitness.Clear();
            DesiredAction = null;
        }

        private void RouletteSelection()
        {
            //TODO: this could all probably be refactored to avoid the toarray calls by using the dictionary directly
            // This method was chosen so I could more easily reuse the original algorithm, which requires [] operators
            double[] wheelFitnessValues = wheelFitness.Values.ToArray();
            double[] wheelSumValues = wheelSum.Values.ToArray();
            Action[] wheelSumKeys = wheelSum.Keys.ToArray();

            double total=0;

            for (int i = 0; i < wheelSum.Values.Count; i++)
            {
                // if we have +ve fitness then divide sum by fitness
                if (wheelFitnessValues[i] > 0)
                    wheelSumValues[i] /= wheelFitnessValues[i];
                total += wheelSumValues[i];
            }
            
            Double randomCounter = PersistantRandom.Instance.NextDouble() * total;
            for (int i = wheelSumValues.Length-1; i >= 0; i--)
            {
                randomCounter -= wheelSumValues[i];
                if (randomCounter <= 0)
                {
                    DesiredAction = wheelSumKeys[i];  
                    break;
                }
            }
        }

        private void RandomSelection(Population MatchSet)
        {
            List<Action> possibleActions = new List<Action>();

            //for XCS, select random action from within MatchSet actions. Do not use Roulette wheel, just plain old random
            foreach (Rule r in MatchSet)
            {
                if (!possibleActions.Contains(r.Action))
                    possibleActions.Add(r.Action);
            }

            DesiredAction = possibleActions[PersistantRandom.Instance.Next(possibleActions.Count())];
        }


        #region Genetic Algorithm Code and variables
        // TODO: expand these and add notifications on change

        [CategoryAttribute("GA"), Browsable(true), ReadOnly(false), BindableAttribute(false), DesignOnly(false), DescriptionAttribute("The average number of iterations before GC is run")]
        public double GeneticAlgorithmThreshold
        {
            get; set;
        }

        [CategoryAttribute("Settings"), Browsable(true), ReadOnly(false), BindableAttribute(false), DesignOnly(false), DescriptionAttribute("The maximum population size before culling of rules occurs")]
        public double MaxPopulation
        {
            get;
            set;
        }

        [CategoryAttribute("GA"), Browsable(true), ReadOnly(false), BindableAttribute(false), DesignOnly(false), DescriptionAttribute("probability of gene exchange")]
        public double Chi
        {
            get;
            set;
        }

        [CategoryAttribute("GA"), Browsable(true), ReadOnly(false), BindableAttribute(false), DesignOnly(false), DescriptionAttribute("probability of mutation")]
        public double Mu
        {
            get;
            set;

        }

        [CategoryAttribute("GA"), Browsable(true), ReadOnly(false), BindableAttribute(false), DesignOnly(false), DescriptionAttribute("Minimum Age of classifier before it can subsume")]
        public double SubsumptionAgeThreshold
        {
            get;
            set;

        }



        private double AverageTimeSinceGA(Population Selected)
        {
            double total = 0;
            foreach (Rule rule in Selected)
            {
                total += rule.Statistics["lastIterationInGA"];
            }
            total /= Selected.Count;

            return IterationCount - total;
        }

        /// <summary>
        /// Mutates a given condition
        /// </summary>
        /// <param name="condition"></param>
        private void MutateCondition(Condition condition)
        {
            for (int i = 0; i < condition.UpperBounds.Count; i++)
            {
                #region generalise only mutation

                //only affects specialised ones
                //can leave it the same, or generalise it.

                if (condition.LowerBounds[i] == condition.UpperBounds[i])
                {
                    if (PersistantRandom.Instance.Next(10) == 1)
                    {
                        condition.LowerBounds[i] = 0;
                        condition.UpperBounds[i] = 1;
                    }
                }
                #endregion

                #region specialise only mutation
                /* OLD CODE:
                //thus can only affect 'don't cares'
                //but can go either way, or leave it the same
                if (_lower == 0 && _upper == 1)
                    switch (PersistantRandom.Instance.Next(3))
                    {
                        case 0:                         //no change
                            break;
                        case 1:
                            //specialise in lower
                            _upper = 0;
                            break;
                        case 2:
                            //specialise in upper
                            _lower = 0;
                            break;
                    }*/
                #endregion
            }
           
        }

        /// <summary>
        /// Mutates a given action
        /// </summary>
        /// <param name="action"></param>
        private void MutateAction(Action action)
        {
            action.Value = ActionFactory.Instance().Value; // replace value with new action's value
        }

        /// <summary>
        /// takes two rules and produces two hybrid rules
        /// </summary>
        private Population Splice(Rule Parent1, Rule Parent2, bool ForceMutate)
        {
            Population result = new Population();

            Rule child1 = (Rule)Parent1.Clone();
            Rule child2 = (Rule)Parent2.Clone();

            // exchange genes
            for (int i = 0; i < child1.Condition.LowerBounds.Count; i++)
            {
                if (PersistantRandom.Instance.NextDouble() < Chi)
                {
                    double tempLow = child1.Condition.LowerBounds[i];
                    double tempUpp = child1.Condition.UpperBounds[i];

                    child1.Condition.LowerBounds[i] = child2.Condition.LowerBounds[i];
                    child1.Condition.UpperBounds[i] = child2.Condition.UpperBounds[i];

                    child2.Condition.LowerBounds[i] = tempLow;
                    child2.Condition.UpperBounds[i] = tempUpp;
                }
            }

            // mutation
            if ((PersistantRandom.Instance.NextDouble() < Mu) || ForceMutate)
            {
                MutateCondition(child1.Condition);
                MutateCondition(child2.Condition);
            }

            if ((PersistantRandom.Instance.NextDouble() < Mu))
            {
                MutateAction(child1.Action);
                MutateAction(child1.Action);
            }

            result.Add(child1);
            result.Add(child2);

            foreach (Rule r in result)
            {
                r.Statistics["age"] = 0;
                r.Numerosity = 1;
                r.Statistics["prediction"] = ((Parent1.Statistics["prediction"] + Parent2.Statistics["prediction"]) / 2.0) * PersistantRandom.Instance.NextDouble();
            }

            return result;
        }

        /// <summary>
        /// Run the GA
        /// </summary>
        public void RunGA(Population Population, Population ActionSet)
        {
            Population rulesToRemove = new Population();
            Population rulesToDecrement = new Population();

            foreach (Rule r in ActionSet)
            {
                r.Statistics["lastIterationInGA"] = IterationCount;
            }

            Population parents = ActionSet.RouletteWheel(2);

            //HACK: return if we don't get enough unique results from roulettewheel to do GA
            if (parents.Count < 2)
                return;
            //else if (parents.Count == 1)        //TODO: Not working! Debug this code - check parent2 is different to parent1
            //{
            //    Rule parent2 = parents[0];
            //    MutateCondition(parent2.Condition);
            //    parents.Add(parent2);
            //}
            //if (parents.Count < 2)
            //    return;

            // build children
            Population children = Splice(parents[0], parents[1], ActionSet.Count==1);

            //if children are unique then add, otherwise increase numerosity of matching rule
            foreach (Rule child in children)
            {
                bool isUnique = true;
                foreach (Rule rule in Population)
                {
                    if ((rule.Condition.Equals(child.Condition))&& (rule.Action.Equals(child.Action)))
                    {
                        rule.Numerosity++;
                        isUnique = false;
                        break;
                    }
                    //subsumption check
                    if ((rule.Condition.Generalized(child.Condition)) && (rule.Action.Equals(child.Action)) && (rule.Statistics["accuracy"] == 1) && (rule.Statistics["age"] > SubsumptionAgeThreshold))
                    {
                        rule.Numerosity++;
                        isUnique = false;
                        break;
                    }
                }

                if (isUnique)
                {
                    child.Statistics["initIter"] = IterationCount;
                    child.Statistics["lastIterationInGA"] = IterationCount;
                    child.Statistics["nicheSize"] = 1;      //initialise to 1 to allow selection for deletion in vote count
                    Population.Add(child);
                }
            }

            // do population size control once per iteration, not once per rule
            double popSize = Population.PopulationSize;

            if (popSize > MaxPopulation)
            {
                double delta = Population.PopulationSize - MaxPopulation;

                for (int i = 0; i < delta; i++)
                {
                    Rule selectedRule = Population.InverseRouletteWheel();

                    if (selectedRule.Numerosity > 1)
                        Population.DecrementCount(selectedRule);    // now decrement rules to be decremented
                    else
                        Population.Remove(selectedRule);            // now do removal on rules to be removed
                }
            }
        }
        
        /// <summary>
        /// Run the subsumption
        /// </summary>
        public void RunSubsumption(Population Population, Population Filtered)
        {
            Population rulesToRemove = new Population();

            foreach (Rule rule in Filtered)
            {
                rulesToRemove.Clear();  //clear for each rule in filtered population

                if ((rule.Statistics["accuracy"] == 1) && (rule.Statistics["age"] > SubsumptionAgeThreshold)) 
                {
                    foreach (Rule otherRule in Population)
                    {
                        if (rule == otherRule)
                            continue;

                        if (rule.Action.Equals(otherRule.Action))
                        {
                            if(rule.Condition.Generalized(otherRule.Condition))
                                rulesToRemove.Add(otherRule);
                        }
                    }
                }

                //DEBUG
                //DEBUG - enable to write output of subsumption for lifetime of LCS
                /*if (rulesToRemove.Count > 0)
                {
                    tw.WriteLine("----------,----------,----------,----------,----------,----------,----------,----------,----------,----------,----------,");
                    tw.WriteLine();
                    tw.WriteLine("Iteration:," + IterationCount.ToString());
                    tw.WriteLine("Condition,Action,Numerosity,Prediction,Error,Fitness,Accuracy,Org_Error,Age,NicheSize,Initial Iteration,lastIterationInGA");
                    tw.WriteLine("Subsumer:");
                    tw.WriteLine(rule.Condition.ToString()+","
                        + rule.Action.ToString() + ","
                        + rule.Numerosity.ToString() + ","
                        + rule.Statistics["prediction"].ToString() + ","
                        + rule.Statistics["error"].ToString() + ","
                        + rule.Statistics["fitness"].ToString() + ","
                        + rule.Statistics["accuracy"].ToString() + ","
                        + rule.Statistics["org_error"].ToString() + ","
                        + rule.Statistics["age"].ToString() + ","
                        + rule.Statistics["nicheSize"].ToString() + ","
                        + rule.Statistics["initIter"].ToString() + ","
                        + rule.Statistics["lastIterationInGA"].ToString() + ","
                        );

                    tw.WriteLine("Subsumed:");
                }*/

                foreach (Rule r in rulesToRemove)
                {
                    //DEBUG - enable to write output of subsumption for lifetime of LCS
                    /*tw.WriteLine(r.Condition.ToString() + "," + r.Action.ToString() + ","
                        + r.Numerosity.ToString() + ","
                        + r.Statistics["prediction"].ToString() + ","
                        + r.Statistics["error"].ToString() + ","
                        + r.Statistics["fitness"].ToString() + ","
                        + r.Statistics["accuracy"].ToString() + ","
                        + r.Statistics["org_error"].ToString() + ","
                        + r.Statistics["age"].ToString() + ","
                        + r.Statistics["nicheSize"].ToString() + ","
                        + r.Statistics["initIter"].ToString() + ","
                        + r.Statistics["lastIterationInGA"].ToString() + ","
                        );
                    */

                    Population.Remove(r);
                }
            }
        }
        #endregion
    }
}
